/*
 * Moseycode
 * Copyright (C) 2008  Tom Gibara
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 */
package com.tomgibara.mosey.portal;

import java.math.BigInteger;

import com.tomgibara.mosey.coding.BadCRC16;
import com.tomgibara.mosey.coding.CRC16;

/**
 * A combination of a 32 bit chamber id, 48 bits of data (split 16/32) and a
 * 16 bit checksum.
 * 
 * @author Tom Gibara
 */

public final class Portal {

	// statics
	
	/**
	 * Has a value of 2^64 and is added to negative longs to generate an
	 * unsigned long.
	 */
	
	private static final BigInteger NORMALIZER = new BigInteger("18446744073709551616");
	
	/**
	 * Used to perform padding in string representations.
	 */
	
	private static final String ZEROS = "000000000000";
	
	/**
	 * Converts a signed integer to its unsigned value and returns it as a string.
	 * 
	 * @param i any integer
	 * @return the integer's unsigned denary representation as a string
	 */
	
	public static String toUnsignedString(int i) {
		return i < 0 ? Long.toString(((long)i) & 0xffffffffL) : Integer.toString(i);
	}
	
	/**
	 * Converts a signed short to its unsigned value and returns it as a string.
	 * 
	 * @param s any short
	 * @return the short's unsigned denary representation as a string
	 */
	
	public static String toUnsignedString(short s) {
		return s < 0 ? Integer.toString(((int)s) & 0xffff) : Short.toString(s);
	}
	
	/**
	 * Converts a signed long to its unsigned value and returns it as a string.
	 * 
	 * @param l any long
	 * @return the long's unsigned denary representation as a string
	 */
	
	public static String toUnsignedString(long l) {
		String s = Long.toString(l);
		if (l >= 0) return s;
		BigInteger b = new BigInteger(s);
		return b.add(NORMALIZER).toString();
	}
	
	public static Portal fromHex(String chamber, String data) throws IllegalArgumentException {
		long chamberId = Long.parseLong(chamber, 16);
		if (chamberId > 0xffffffffL || chamberId < 0L) throw new IllegalArgumentException("chamberId too large or negative");
		//TODO should do more thorough job
		int i = data.indexOf(':');
		if (i == -1) {
			long longData = Long.parseLong(data, 16);
			if (longData > 0xffffffffffffL || longData < 0L) throw new IllegalArgumentException("longData too large or negative");
			return new Portal((int)chamberId, longData);
		} else {
			int shortData = Integer.parseInt(data.substring(0, i), 16);
			if (shortData > 0xffff || shortData < 0) throw new IllegalArgumentException("shortData too large or negative");
			long intData = Long.parseLong(data.substring(i+1), 16);
			if (intData > 0xffffffffL || intData < 0L) throw new IllegalArgumentException("intData too large or negative");
			return new Portal((int)chamberId, (short)shortData, (int)intData);
		}
	}
	
	// fields
	
	private int chamberId;
	private short shortData;
	private int intData;
	private short checksum;
	private boolean checksumValid;

	// constructors
	
	public Portal() {
		chamberId = -1;
		shortData = -1;
		intData = -1;
	}
	
	public Portal(int chamberId, short shortData, int intData) {
		this.chamberId = chamberId;
		this.shortData = shortData;
		this.intData = intData;
	}
	
	public Portal(int chamberId, long longData) {
		this.chamberId = chamberId;
		this.shortData = (short) (longData >> 32);
		this.intData = (int) longData;
	}
	
	// accessors
	
	public void setChamberId(int chamberId) {
		this.chamberId = chamberId;
		checksumValid = false;
	}
	
	public int getChamberId() {
		return chamberId;
	}
	
	public void setShortData(short shortData) {
		this.shortData = shortData;
		checksumValid = false;
	}
	
	public short getShortData() {
		return shortData;
	}
	
	public void setIntData(int intData) {
		this.intData = intData;
		checksumValid = false;
	}
	
	public int getIntData() {
		return intData;
	}
	
	public long getLongData() {
		//TODO find out why this doesn't work
		//return ((((long)shortData) << 32) & 0x0000ffff00000000L) | (((long)intData) & 0x00000000ffffffff);
		long data = 0L;
		data |= shortData & 0xffffL;
		data <<= 32;
		data |= intData & 0xffffffffL;
		return data;
	}
	
	public void setChecksum(short checksum) {
		this.checksum = checksum;
		checksumValid = true;
	}
	
	public short getChecksum() {
		if (!checksumValid) {
			if (chamberId>=0 && chamberId < 16) {
				//AWFUL LEGACY HACK - early development barcodes were issued with the wrong CRC!!!
				BadCRC16 crc = new BadCRC16();
				crc.addInt(chamberId);
				crc.addShort(shortData);
				crc.addInt(intData);
				checksum = crc.checksum();
			} else {
				CRC16 crc = new CRC16();
				crc.addInt(chamberId);
				crc.addShort(shortData);
				crc.addInt(intData);
				checksum = crc.checksum();
			}
			checksumValid = true;
		}
		return checksum;
	}

	public String getChamberHex(boolean padded) {
		String s = Integer.toHexString(chamberId);
		if (padded && s.length() < 8) {
			StringBuilder sb = new StringBuilder(8);
			sb.append(ZEROS.substring(s.length(), 8));
			sb.append(s);
			return sb.toString();
		} else {
			return s;
		}
	}
	
	public String getDataHex(boolean padded, boolean split) {
		if (split) {
			StringBuilder sb = new StringBuilder(13);
			String s = Integer.toHexString(shortData & 0xffff);
			if (padded && s.length() < 4) sb.append(ZEROS.substring(s.length(), 4));
			sb.append(s);
			sb.append(':');
			String t = Integer.toHexString(intData);
			if (padded && t.length() < 8) sb.append(ZEROS.substring(t.length(), 8));
			sb.append(t);
			return sb.toString();
		} else {
			String s = Long.toHexString( getLongData() );
			if (padded && s.length() < 12) {
				StringBuilder sb = new StringBuilder(12);
				sb.append(ZEROS.substring(s.length(), 12));
				sb.append(s);
				return sb.toString();
			} else {
				return s;
			}
		}
	}
	
	// methods
	
	public boolean isValid() {
		return chamberId >= 0 && shortData >= 0;
	}
	
	public boolean isAnonymous() {
		return intData < 0;
	}
	
	// object methods
	
	@Override
	public boolean equals(Object obj) {
		if (obj == this) return true;
		if (!(obj instanceof Portal)) return false;
		Portal that = (Portal) obj;
		return
			this.chamberId == that.chamberId &&
			this.shortData == that.shortData &&
			this.intData == that.intData;
	}

	@Override
	public int hashCode() {
		return getChecksum();
	}
	
	@Override
	public String toString() {
		return String.format("[Portal chamber: %d instance: %d depositor: %d]", chamberId, shortData, intData);
	}

	// private utility methods
	
}
